/**
 * \file   bootloader.h
 * \author fabianjsm
 * \date   Noviembre de 2011
 * 
 *  Este archivo es parte del proyecto MPHB, aplicación que se comunica
 * a traves del puerto USB con el bootloader en un microcontrolador PIC
 * de la familia 18, 24 o 32 para programarlo utilizando el protocolo
 * HID-Bootloader de Microchip.
 * 
 *  El codigo fuente esta diponible a traves del repositorio SVN de
 * Google:
 * 
 *      http://mphb.googlecode.com/svn/trunk
 * 
 * Fabian Jesus Machado (fabianjsm@gmail.com, twitter@fabianjsm).
 * Alumno de Ingenieria Electronica, Universidad Nacional De Misiones
 * (U.Na.M), Facultad de Ingeniería de Obera.
 * 
 */
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <time.h>
#include <getopt.h>
#include <libusb-1.0/libusb.h>
#include "bootloader.h"
#include "mphb.h"

using namespace std;

// Lista de opciones cortas para getopt
const char *const vop = "hv";

// Lista de opciones largas para getopt
const struct option vopl[] = {
	{"detach-usb",  1, NULL, OP_DETACH_USB},
	{"detect-only", 0, NULL, OP_DETECT_ONLY},
	{"erase",       1, NULL, OP_ERASE},
	{"erase-only",  0, NULL, OP_ERASE_ONLY},
	{"help",        0, NULL, OP_HELP},
	{"no-program",  0, NULL, OP_NO_PROGRAM},
	{"pid",         1, NULL, OP_PID},
	{"show-config", 0, NULL, OP_SHOW_CONFIG},
	{"show-info",   0, NULL, OP_SHOW_INFO},
	{"verbose",     1, NULL, OP_VERBOSE},
	{"version",     0, NULL, OP_VERSION},
	{"vid",         1, NULL, OP_VID},
	{NULL,          0, NULL, 0}
};

environment *env;

/**
 * Esta clase provee un metodo de salida para logs
 */
class _info {
public:
	template<class T>
	_info &operator<<(T s)
	{
		if(env->verbose)
			clog << s;
		return *this;
	};
} info;

/**
 * Constructor de la clase environment
 */
environment::environment()
{
	/* Configuracion */
	detach = DETACH_USB;
	detect_only = false;
	show_config = false;
	show_info = false;
	verbose = false;
	erase = false;
	erase_only = false;
	program = true;
	vid = VENDOR_ID;
	pid = PRODUCT_ID;
}

/**
 * Construye un std::string a partir de la cadena de formatos \param s.
 * \return std::string con el formato especificado
 */
string mkstring(const char *s, ...)
{
	va_list arg;
	va_start(arg, s);
	char buf[0x400];
	vsprintf(buf, s, arg);
	va_end(arg);
	return string(buf);
}

/**
 * Retardo de \param n segundos
 */
void delay_sec(int n)
{
	clock_t t = clock () + n * CLOCKS_PER_SEC;
	while(clock() < t)
		;
}

/**
 * Convierte la cadena \param s en su equivalente numerico base 16.
 * Ejemplo: Recibe "20" y retorna 32
 */
uint32_t HexToInt(string s)
{
	uint32_t h = 0, k = 1;
	int len = s.length();
	for (int i=0; i<len; i++) {
		char c = s[len-i-1];
		if ((c >= 'A') && (c <= 'F'))
			c -= 'A' - 10;
		else if ((c >= 'a') && (c <= 'f'))
			c -= 'a' - 10;
		else if((c >= '0') && (c <= '9'))
			c -= '0';
		else
			throw(string("Coversion HexToInt"));
		h += c * k;
		k *= 16;
	}
	return h;
}

bool si_no(const char *s)
{
	int i;
	string str;
	const char *si[] = {"yes", "true", "si", 0};
	const char *no[] = {"not", "false", "no", 0};
	for(i=0; s[i]; i++)
		if(s[i] >= 'A' && s[i] <= 'Z')
			str += s[i] ^ 0x20;
		else
			str += s[i];
	for(i=0; si[i]; i++)
		if(str == si[i])
			return true;
	for(i=0; no[i]; i++)
		if(str == no[i])
			return false;
	throw(string("se esperaba true o false"));
}

const char *str_si_no(bool b)
{
	return b? "Si": "No";
}

/**
 * Muestra la configuracion actual de la aplicacion
 */
void environment::ShowConfig()
{
	printf("Configuracion de la aplicacion:\n");
	printf("detach-usb:             %s\n",   str_si_no(detach));
	printf("verbose:                %s\n",   str_si_no(verbose));
	printf("detect-only:            %s\n",   str_si_no(detect_only));
	printf("erase:                  %s\n",   str_si_no(erase));
	printf("erase-only:             %s\n",   str_si_no(erase_only));
	printf("program:                %s\n",   str_si_no(program));
	printf("ProductID:              %04X\n", pid);
	printf("VendorID:               %04X\n", vid);
}

/**
 * Muestra la informacion del bootloader detectado
 */
void environment::ShowInfo()
{
	printf("MCHPFSUSB:\n");
	printf("Familia:                PIC%d\n", env->familia);
	printf("Regiones de memoria:    %d\n",    env->regiones);
	printf("Longitud de palabra:    %d\n",    env->bytes_por_dir);
	printf("Bytes por paquete:      %d\n",    env->bytes_por_pack);
}

void environment::SetConfig(uint op, const char *var, void *val)
{
	char *s = reinterpret_cast<char *>(val);
	union {
		uint i;
		bool b;
	} u;
	try {
		int len = strlen(s);
		if(op & OP_NUMERICO) {
			if(len > 4)
				throw(mkstring("Desbordamiento", var));
			u.i = HexToInt(s);
		} else if(op & OP_SI_NO) {
			u.b = si_no(s);
		}
	}
	catch (string err) {
		throw(mkstring("%s (parametro '%s')", err.c_str(), var));
	}
	switch(op) {
	case OP_PID:         env->pid = u.i;     break;
	case OP_VID:         env->vid = u.i;     break;
	case OP_DETACH_USB:  env->detach = u.b;  break;
	case OP_VERBOSE:     env->verbose = u.b; break;
	}
}

/**
 *  Retorna una descripcion del error en funcion de su numero retornado
 * por la libreria libusb.
 */
string usb_err_description(int nerr, const char *fnc)
{
	const char *s;
	switch (nerr) {
	case LIBUSB_ERROR_TIMEOUT:
		s = "Tiempo de espera agotado";
		break;
	case LIBUSB_ERROR_PIPE:
		s = "Error en pipe (el dispositivo no soporta la operacion)";
		break;
	case LIBUSB_ERROR_NO_DEVICE:
		s = "No se encuentra el dispositivo (fue desconectado?)";
		break;
	case LIBUSB_ERROR_NOT_FOUND:
		s = "Dispositivo no funciona";
		break;
	case LIBUSB_ERROR_BUSY:
		s = "Recurso ocupado";
		break;
	case LIBUSB_ERROR_OVERFLOW:
		s = "Desbordamiento";
		break;
	case LIBUSB_ERROR_IO:
		s = "Error de entrada/salida";
		break;
	case LIBUSB_ERROR_INVALID_PARAM:
		s = "Parametro invalido";
		break;
	case LIBUSB_ERROR_ACCESS:
		s = "Acceso denegado (permiso insuficiente)";
		break;
	case LIBUSB_ERROR_INTERRUPTED:
		s = "Interrupcion de llamada al sistema";
		break;
	case LIBUSB_ERROR_NO_MEM:
		s = "Falta memoria";
		break;
	case LIBUSB_ERROR_NOT_SUPPORTED:
		s = "Operacion no implementada en esta plataforma";
		break;
	default:
		s = "Error inesperado";
		break;
	}
	return mkstring("Llamando a libusb_%s: %s", fnc, s);
}

/**
 *  Devuelve el buffer de memoria asociado a una region.
 *  Se recurre a esto para no guardar los datos en la estructura
 * MEMORY_REGION, ya que esta puede ser pasada tal cual al bootloader
 * (El miembro QueryResults de la union BOOTLOADER_COMMAND pasada al
 * bootloader, posee un el campo MemoryRegions que es del tipo
 * MEMORY_REGION).
 */
void setDataMemoryRegion(uint8_t region, uint8_t* p)
{
	if(region < 6)
		env->vData[region] = p;
}

/**
 *  Devuelve el buffer de memoria asociado a una region.
 *  Se recurre a esto para no guardar los datos en la estructura
 * MEMORY_REGION, ya que esta puede ser pasada tal cual al bootloader
 * (El miembro QueryResults de la union BOOTLOADER_COMMAND pasada al
 * bootloader, posee un el campo MemoryRegions que es del tipo
 * MEMORY_REGION).
 */
uint8_t *getDataMemoryRegion(uint8_t region) 
{
	return (region < 6)? env->vData[region]: 0;
}

/**
 * Abre el dispositivo HID USB buscandolo por su VendorID-ProductID.
 * Ambos parametros son modificables desde la lina de comandos.
 */
void buscar_hid(void)
{
	int i, r;
	
	env->regiones=0;
	env->bytes_por_dir =0;
	env->reg_mem = new MEMORY_REGION[MAX_DATA_REGIONS];

	for (i=0;i<MAX_DATA_REGIONS;i++)
		setDataMemoryRegion(i,0);

	if(libusb_init(NULL))
		throw(string("No se pudo inicializar libusb"));

	env->devh = libusb_open_device_with_vid_pid(NULL, env->vid, env->pid);
	
	if(env->devh == NULL)
		throw(string("No se pudo establecer conexion con el bootloader,"
			" verifique si esta conectado y en modo BOOT"));

	if(env->detach)
		libusb_detach_kernel_driver(env->devh, 0);

	if((r=libusb_set_configuration(env->devh, HID_CONFIGURATION)) != 0)
		throw(usb_err_description(r, "set_configuration"));

	if ((r=libusb_claim_interface(env->devh, 0)) != 0)
		throw(usb_err_description(r, "claim_interface"));
}

/**
 *  Reconoce al bootloader y obtiene parametros del microcontrolador,
 * tales como su familia, la cantidad de regiones que posee, etc.
 */
void detectar_bootloader()
{
	int n = 0;
	int BytesReceived = 0;
	uint8_t buf[64];
	BOOTLOADER_COMMAND rta;
	uint8_t i;
	int r;

	for (int i=0;i<64;i++) {
		buf[i]=0;
		rta.RawData[i]=0;
	}

	buf[0] = QUERY_DEVICE;

	if((r=libusb_bulk_transfer(env->devh, ENDPOINT_INT_OUT, buf, 64, &n, TIMEOUT)) != 0)
		throw(usb_err_description(r, "bulk_transfer"));

	if((r=libusb_bulk_transfer(env->devh, ENDPOINT_INT_IN,  rta.RawData, 64, &BytesReceived, TIMEOUT)) != 0)
		throw(usb_err_description(r, "bulk_transfer"));

	env->regiones = 0;
	for (i=0; i<MAX_DATA_REGIONS; i++) {
		if (rta.QueryResults.MemoryRegions[i].Type == MEMORY_REGION_END)
			break;
		env->reg_mem[i].Type = rta.QueryResults.MemoryRegions[i].Type;
		env->reg_mem[i].Address = rta.QueryResults.MemoryRegions[i].Address;
		env->reg_mem[i].Size = rta.QueryResults.MemoryRegions[i].Size;
		env->regiones++;
	}
	
	switch (rta.QueryResults.DeviceFamily) {
	case DEVICE_FAMILY_PIC18:
		env->bytes_por_dir = 1;
		env->familia = 18;
		break;
	case DEVICE_FAMILY_PIC24:
		env->bytes_por_dir = 2;
		env->familia = 24;
		break;
	case DEVICE_FAMILY_PIC32:
		env->bytes_por_dir = 1;
		env->familia = 32;
		break;
	}

	for (i=0; i<env->regiones; i++) {
		uint8_t *pData = (uint8_t*)
			malloc((env->reg_mem[i].Size + 1) * env->bytes_por_dir);
		if(!pData)
			throw(string("Falta memoria"));
		setDataMemoryRegion(i, pData);
	}
	
	env->bytes_por_pack = rta.QueryResults.BytesPerPacket;
	
	libusb_device *dev = libusb_get_device(env->devh);
	
	info << mkstring("USB [%04X:%04X]: PIC%d MCHPFSUSB bootloader"
		" (bus %d, device %d)\n",
		env->vid, env->pid, env->familia, libusb_get_bus_number(dev),
		libusb_get_device_address(dev));
}

/**
 * Borra el microcontrolador
 */
void borrar()
{
	BOOTLOADER_COMMAND cmd, rta;
	int n, i, r;
	
	cmd.EraseDevice.Command = ERASE_DEVICE;
	
	if((r=libusb_bulk_transfer(env->devh, ENDPOINT_INT_OUT, cmd.RawData, 64, &n, TIMEOUT)) != 0)
		throw(usb_err_description(r, "bulk_transfer"));

	for (i=0; i<30; i++) {
		delay_sec(1);
		cmd.QueryDevice.Command = QUERY_DEVICE;
		if(libusb_bulk_transfer(env->devh, ENDPOINT_INT_OUT, cmd.RawData, 64, &n, TIMEOUT))
			continue;
		if(libusb_bulk_transfer(env->devh, ENDPOINT_INT_IN, rta.RawData, 64, &n, TIMEOUT))
			continue;
		info << "La operacion de borrado se concreto con exito\n";
		return;
	}
	
	throw(string("Fallo el intento de borrado"));
}

/**
 * Programa una \param region especifica del microcontrolador.
 */
void programar_region(int region)
{
	BOOTLOADER_COMMAND cmd;
	uint8_t* p;
	DWORD dir, limite;
	uint32_t size;
	uint8_t i, pos;
	bool omitir, omitido;
	int n;
	dir = env->reg_mem[region].Address;
	size = env->reg_mem[region].Size;
	limite = dir + size;
	pos = 1;
	p = getDataMemoryRegion(region);
	omitido = false;
	omitir = true;
	
	while (dir < limite) {
		size_t dim = sizeof(cmd.ProgramDevice.Data);
		cmd.ProgramDevice.Command = PROGRAM_DEVICE;
		cmd.ProgramDevice.Address = dir;
		for (i=0; i<env->bytes_por_pack; i++) {
			uint8_t data = *p++;
			cmd.ProgramDevice.Data[i+(dim-env->bytes_por_pack)] = data;
			if (data != 0xFF) {
				if (env->bytes_por_dir == 2) {
					if (dir & 1) {
						if (pos != 2)
							omitir = false;
					} else
						omitir = false;
				} else
					omitir = false;
			}
			if(++pos > env->bytes_por_dir) {
				++dir;
				pos = 1;
			}
			if(dir >= limite) {
				uint8_t n;
				i++;
				// movemos los datos a la parte baja del paquete
				// si hay pocos datos (n<i) rellenamos con 0
				for (n=0; n<sizeof(cmd.ProgramDevice.Data); n++) {
					int y = dim - n - 1;
					int x = y - env->bytes_por_pack;
					unsigned char *datos = cmd.ProgramDevice.Data;
					datos[y] = (n<i)? datos[x]: 0;
				}
				break;
			}
		}
		cmd.ProgramDevice.BytesPerPacket = i;
		if(omitir)
			omitido = true;
		else {
			if(omitido) {
				BOOTLOADER_COMMAND cmd;
				cmd.ProgramComplete.Command = PROGRAM_COMPLETE;
				if(libusb_bulk_transfer(env->devh, ENDPOINT_INT_OUT, cmd.RawData, 64, &n, TIMEOUT))
					throw(string("La programacion fallo"));
				omitido = false;
			}
			if(libusb_bulk_transfer(env->devh, ENDPOINT_INT_OUT, cmd.RawData, 64, &n, TIMEOUT))
				throw(string("La programacion fallo"));
		}	
		omitir = true;
	}
	//  Ahora que terminamos de grabar todas las direcciones de esta
	// region de memoria tenemos que enviar el comando PROGRAM_COMPLETE.
	cmd.ProgramComplete.Command = PROGRAM_COMPLETE;
	if (libusb_bulk_transfer(env->devh, ENDPOINT_INT_OUT, cmd.RawData, 64, &n, TIMEOUT))
		throw(string("La programacion fallo"));
}

/**
 * Programa todas las regiones del microcontrolador
 */
void programar()
{
	// Paso 0: programamos la region de configuracion, y
	// Paso 1: las demas regiones.
	for(int paso=0; paso<2; paso++)	{
		for(int region=0; region < env->regiones; region++) {
			uint8_t tipo = env->reg_mem[region].Type;
			if(paso == 0) {
				if(tipo != MEMORY_REGION_CONFIG)
					continue;
			} else if(tipo == MEMORY_REGION_CONFIG)
				continue;
			programar_region(region);
		}
	}
	info << "Todas la regiones fueron programadas con exito\n";
}

/**
 * Abre el archivo con formato HEX Intel.
 * Suponemos que el dispositivo fue previamente detectado y se
 * determino la cantidad de regiones y sus longitudes.
 */
void abrir_hex(const char *archivo)
{
	FILE *f; // descriptor de archivo
	char linea[4096]; // buffer para la linea actual
	uint32_t dir_ext = 0; // parte alta de la direccion absoluta
	bool HexEOF = false; // bandera: final de archivo hex
	string str, buf;
	int nlinea=0;

	if((f = fopen(archivo, "r"))  == NULL)
		throw(mkstring("No se puede abrir el archivo '%s'\n", archivo));
		
	while((fgets(linea, 4096, f) != NULL) && !HexEOF) {
		++nlinea;
		char *p;
		int len = strlen(linea);
		for(p = linea + len - 1; (*p == '\r' || *p == '\n') && len; --p)
			--len, *p=0;
		if(!len)
			continue;
		if(linea[0] != ':')
			throw(mkstring("Linea %d no posee fomato HEX", nlinea));
		string str = linea+1;
		
		uint8_t lng = HexToInt(str.substr(0, 2)); // bytes de datos
		uint16_t dir = HexToInt(str.substr(2, 4)); // direccion
		uint8_t tipo = HexToInt(str.substr(6, 2)); // tipo de linea
		buf = str.substr(8, lng*2);	 // datos de la linea
		
		/* Verificamos el checksum */
		uint8_t chk	= HexToInt(str.substr((lng*2)+8, 2)); // checksum
		for(int i=0; i<(lng+4); i++)
			chk += HexToInt(str.substr(i*2, 2));
		if(chk) {
			fclose(f);
			throw("Checksum corrupto");
		}
		
		switch(tipo) {
		case HEX_FILE_EXTENDED_LINEAR_ADDRESS:
			dir_ext = HexToInt(buf);
			break;
		case HEX_FILE_EOF:
			HexEOF = true;
			break;
		case HEX_FILE_DATA: {
				uint32_t dir_abs = (dir_ext << 16) + dir;
				// Recorremos las regiones de memorias para determinar
				// a cual de ellas pertenecen los datos actuales en
				// funcion de su direccion de memoria.
				for (int i=0; i < env->regiones; i++) {
					// En env->reg_mem solo tenemos la descripcion de
					// la region, con getDataMemoryRegion obtenemos su
					// buffer de datos.
					uint32_t base;
					base = env->reg_mem[i].Address * env->bytes_por_dir;
					if(dir_abs < base)
						continue;
					if(dir_abs >= ((env->reg_mem[i].Address +
					  env->reg_mem[i].Size) * env->bytes_por_dir))
						continue;
					uint8_t *pData, *limite, *p;
					pData = getDataMemoryRegion(i);
					limite = (uint8_t*)(pData + ((env->reg_mem[i].Size
					  + 1) * env->bytes_por_dir));
					p = (uint8_t*)(pData + dir_abs - base);
					// Copiamos los datos a la region de memoria
					for (int j=0; j<lng; j++) {
						if (p >= limite)
							break;
						*p++ = (uint8_t)HexToInt(buf.substr(j*2, 2));
					}
					break;
				}
			}
		}
	}
	
	fclose(f);
	
	if(!HexEOF)
		throw(string("El archivo HEX no posee registro EOF"));
}

/**
 * Despliega un breve mensaje informativo hacerca de la aplicacion.
 */
void usar()
{
	cout <<
		"Usar: mphb [opciones] archivo.hex\n"
		"Opciones:\n"
		" --erase         Borrar el dispositivo\n"
		" --no-program    No programar el dispositivo\n"
		" --show-config   Muestra el estado de configuracion actual\n"
		" --verbose       Muestra informaciones\n"
		" --version       Version del programa\n"
		" --vid           Especifica otro VendorID\n"
		" --pid           Especifica otro ProductID\n"
		" --detect-only   Terminar despues de detectar el bootloader\n"
		" --erase-only    Terminar despues de borrar\n"
		" --detach-usb    Desconectar USB para reconfigurarlo (LNX)\n"
		" --help          Muestra esta ayuda y termina\n"		
	;
}

/**
 * Muestra la version del programa y termina
 */
void version()
{
	cout << "mphb version " STR_VERSION " (fabianjsm)\n";
}

/**
 * Procesar linea de comandos
 */
int args(int argc, char **argv)
{
	int i, op;
	try {
		while((op = getopt_long(argc, argv, vop, vopl, &i)) != -1) {
			switch(op) {
			case 'h':
			case OP_HELP:
				usar();
				return 1;
			case 'v':
			case OP_VERSION:
				version();
				return 1;
			case OP_PID:
			case OP_VID:
			case OP_DETACH_USB:
			case OP_VERBOSE:
				env->SetConfig(op, vopl[i].name, optarg);
				break;
			case OP_DETECT_ONLY:
				env->detect_only = true;
				env->verbose = true;
				break;
			case OP_ERASE:
				env->erase = true;
				break;
			case OP_ERASE_ONLY:
				env->erase_only = true;
				break;
			case OP_SHOW_CONFIG:
				env->show_config = true;
				break;
			case OP_SHOW_INFO:
				env->show_info = true;
				break;
			case OP_NO_PROGRAM:
				env->program = false;
				break;
			default:
				throw(string("error sintactico"));
			}
		}
		switch(argc - optind) {
		case 0:
			break;
		case 1:
			env->archivo_hex = argv[optind];
			break;
		default:
			throw(string("parametros inesperados"));
		}
	}
	catch(string e) {
		throw(mkstring("%s en linea de comandos", e.c_str()));
	}
	return 0;
}

/**
 * Funcion principal
 */
int main(int argc, char **argv)
{
	env = new environment();
	try {
		if(args(argc, argv))
			return 0;
		if(env->show_config)
			env->ShowConfig();
		buscar_hid();
		detectar_bootloader();
		if(env->show_info)
			env->ShowInfo();
		if(env->detect_only)
			return 0;
		if(env->archivo_hex.length())
			abrir_hex(env->archivo_hex.c_str());
		if(env->erase || env->erase_only)
			borrar();
		if(env->erase_only)
			return 0;
		if(env->program && env->archivo_hex.length())
			programar();
	}
	catch(string e) {
		cerr << "Error: " << e << endl;
		return 1;
	}
	return 0;
}
